package org.rayson.server.http;

import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

import org.rayson.server.transport.ServerConnection;
import org.rayson.share.http.HttpConstants;
import org.rayson.share.http.HttpMessageParser;

public class ServerRequestReader implements AsyncRequestReader {

	private static enum State {
		NEW, START_LINE, HEADERS, BODY;
	}

	private static final int INDEX_LINE_NOT_FOUND = -1;
	private static final int INDEX_EMPTY_LINE_FOUND = 0;

	private static final int BUFFER_SIZE = 10240;
	private static final int EOF = -1;
	private final ServerConnection connection;
	private final ByteBuffer bb;
	private int consumedIndex;

	private HttpServerRequest lastMessage;

	private final List<HttpServerRequest> receivedMsgs;
	private State state;

	private ByteBuffer lastBodyBuffer;

	public ServerRequestReader(final ServerConnection connection) {
		super();
		this.connection = connection;
		bb = ByteBuffer.allocate(BUFFER_SIZE);
		receivedMsgs = new ArrayList<>();
		state = State.NEW;
	}

	/**
	 * Take an line out of the underling buffer.
	 * 
	 * @param count
	 *            Bytes count, start from consumed position.
	 * @return
	 */
	private byte[] consumeLine(final int count) {
		final byte[] dst = new byte[count];
		int dstI = 0;
		for (int i = consumedIndex; i < consumedIndex + count; i++) {
			dst[dstI++] = bb.get(i);
		}
		consumedIndex += (count + 2);
		return dst;
	}

	/**
	 * @return Next CRLF position, start form consumed position. Or -1, if not
	 *         found.
	 */
	private int nextLineIndex() {
		int b;
		for (int i = consumedIndex; i < bb.position() - 1; i++) {
			b = bb.get(i);
			if (b == HttpConstants.CR && bb.get(i + 1) == HttpConstants.LF) {
				return i - consumedIndex;
			}
		}
		return INDEX_LINE_NOT_FOUND;
	}

	@Override
	public HttpServerRequest[] read() throws IOException {

		if (connection.getChannel().read(bb) == EOF)
			throw new EOFException();

		int nli;
		OUTER: while (true) {

			switch (state) {

			case NEW:
				lastMessage = new HttpServerRequest();
				state = State.START_LINE;

			case START_LINE:
				nli = nextLineIndex();
				switch (nli) {
				case INDEX_LINE_NOT_FOUND:

					break OUTER;
				case INDEX_EMPTY_LINE_FOUND:
					// TODO: HTTP Message format error.
				default:
					lastMessage.setStartLine(HttpMessageParser
							.parseRequestLine(consumeLine(nli)));
					state = State.HEADERS;
					break;
				}

			case HEADERS:
				INNER: while (true) {
					nli = nextLineIndex();
					switch (nli) {
					case INDEX_LINE_NOT_FOUND:
						break OUTER;
					case INDEX_EMPTY_LINE_FOUND:
						consumedIndex += 2;
						lastBodyBuffer = ByteBuffer.allocate(lastMessage
								.getBodyLength());
						state = State.BODY;
						break INNER;
					default:
						lastMessage.addHeader(HttpMessageParser
								.parseHeader(consumeLine(nli)));
						break;
					}
				}

			case BODY:

				final int count = Math.min(lastBodyBuffer.remaining(),
						bb.position() - consumedIndex);
				for (int i = 0; i < count; i++) {
					lastBodyBuffer.put(bb.get(consumedIndex++));
				}
				if (!lastBodyBuffer.hasRemaining()) {
					lastMessage.setBody(lastBodyBuffer.array());
					receivedMsgs.add(lastMessage);
					state = State.NEW;
				}
				break;

			default:
				break;
			}
		}

		if (receivedMsgs.isEmpty())
			return null;
		else {
			final HttpServerRequest[] result = receivedMsgs
					.toArray(new HttpServerRequest[0]);
			receivedMsgs.clear();
			return result;
		}
	}
}